package core;

import fileoperation.*;
import gitobject.*;
import repository.Repository;

import java.io.*;
import java.util.*;



public class JitMerge {

    /**
     * Merge the specified branch into the current branch
     * @param anotherBranchName
     * @throws Exception
     */
    public static void merge(String anotherBranchName) throws Exception {

        // If the specified branch does not exist
        if( !FileStatus.branchExist(anotherBranchName) ){
            System.out.println("merge: " + anotherBranchName +  " - not something we can merge");
            return;
        }

        String currentBranchName = JitBranch.getCurBranch().branchName;

        // Merge itself
        if(anotherBranchName.equals(currentBranchName)){
            System.out.println("Already up to date.");
            return;
        }

        // Deserialize the commit of the current branch
        Scanner input = new Scanner(new File(Repository.getGitDir() + File.separator + "refs" + File.separator
                + "heads" + File.separator + currentBranchName));
        String head = input.nextLine();
        Commit currentCommit = Commit.deserialize(head);

        // Deserialize the commit of the another branch
        input = new Scanner(new File(Repository.getGitDir() + File.separator + "refs" + File.separator +
                "heads" + File.separator + anotherBranchName));
        String anotherBranchCommitID = input.nextLine();
        Commit anotherBranchCommit = Commit.deserialize(anotherBranchCommitID);

        // Find the last common ancestor of two branch
        Commit lastCommonAncestor = LCA(anotherBranchName, JitBranch.getCurBranch().branchName);

        HashMap<String, String> currentBlobMap = loadFile(currentCommit.getTree());
        HashMap<String, String> anotherBlobMap = loadFile(anotherBranchCommit.getTree());
        HashMap<String, String> lcaBlobMap = loadFile(lastCommonAncestor.getTree());

        if(lastCommonAncestor.isEquals(anotherBranchCommit)) {

            // Case 1: another branch (that is, the branch corresponding to the input anotherBranchName) is LCA

            System.out.println("Already up to date.");

        } else if(lastCommonAncestor.isEquals(currentCommit)){

            // Case 2: the current branch is LCA

            JitCheckout.syncronize(anotherBranchCommitID); // Change the workspace
            BufferedWriter out = new BufferedWriter(new FileWriter(Repository.getGitDir() + File.separator + "refs"
                        + File.separator + "heads" + File.separator + JitBranch.getCurBranch().branchName));
            out.write(anotherBranchCommitID);
            out.close();

            StringBuilder printContent = new StringBuilder("");
            int[] changeRes = compareTwoChanges(anotherBlobMap, lcaBlobMap, printContent);
            System.out.print("\nFast-forward\n" + printContent);
            System.out.println(changeRes[0] + " files changed, " + changeRes[1] + " insertions(+), " + changeRes[2] + " deletions(-)");

        }else{

            // Case 3: LCA is neither the current branch nor another branch

            StringBuilder printContent = new StringBuilder("");
            ArrayList<String> conflictList = new ArrayList<>();
            HashMap<String,String> changeFiles = new HashMap<>();
            int[] changeRes = compareThreeChanges(lcaBlobMap, anotherBlobMap, currentBlobMap, printContent,conflictList,changeFiles);

            if(conflictList.size() != 0){  // If there are conflicting files
                System.out.println("fatal - The following files have conflicts and cannot be merged automatically:");
                conflictList.forEach(filename->System.out.println(filename)); // Print conflicting file names
            }else{

                for(String key: changeFiles.keySet()){
                    if(changeFiles.get(key).equals("add")){
                        String fileContent = Blob.deserialize(anotherBlobMap.get("key")).getValue();
                        FileCreation.createFile(Repository.getWorkTree(),key,fileContent);
                    }else if(changeFiles.get(key).equals("delete")){
                        FileDeletion.deleteFile(Repository.getWorkTree() + File.separator + key);
                    }else{
                        String fileContent = Blob.deserialize(anotherBlobMap.get("key")).getValue();
                        BufferedWriter out = new BufferedWriter(new FileWriter(Repository.getWorkTree() + File.separator + key));
                        out.write(fileContent); // Overlay
                        out.close();
                    }
                }

                JitAdd.add(new File(Repository.getWorkTree()));
                JitCommit merge = new JitCommit();
                merge.commit("merge " + anotherBranchName + " and " + JitBranch.getCurBranch().branchName);

                System.out.println("Merge and commit succeeded");
                System.out.println(printContent);
                System.out.println(changeRes[0] + " files changed, " + changeRes[1] + " insertions(+), " + changeRes[2] + " deletions(-)");
            }

        }

    }


    /**
     * find the last common ancestor of two branches
     * @param branchName1
     * @param branchName2
     * @return Commit
     */
    public static Commit LCA(String branchName1, String branchName2) throws IOException {

        File branch1File = new File(Repository.getGitDir() + File.separator + "refs" + File.separator + "heads"
                + File.separator + branchName1);
        File branch2File = new File(Repository.getGitDir() + File.separator + "refs" + File.separator + "heads"
                + File.separator + branchName2);

        if(!branch1File.exists() || !branch2File.exists())
            return null;

        Scanner input = new Scanner(branch1File);
        String branch1CommitID = input.nextLine();
        input = new Scanner(branch2File);
        String branch2CommitID = input.nextLine();

        Commit branch1Commit = Commit.deserialize(branch1CommitID), branch2Commit = Commit.deserialize(branch2CommitID);
        Commit branch1CommitBackup = Commit.deserialize(branch1CommitID), branch2CommitBackup = Commit.deserialize(branch2CommitID);

        while(branch1Commit == null && branch2Commit !=null ||!branch1Commit.isEquals(branch2Commit)){
            if(branch1Commit == null){
                branch1Commit = branch2CommitBackup;
            }
            else{
                branch1CommitID = branch1Commit.getParent();
                branch1Commit = Commit.deserialize(branch1CommitID);
            }

            if(branch2Commit == null) {
                branch2Commit = branch1CommitBackup;
            }
            else{
                branch2CommitID = branch2Commit.getParent();
                branch2Commit = Commit.deserialize(branch2CommitID);
            }
        }

        return branch1Commit;
    }


    /**
     * Determine the map containing the corresponding file of the specified commitID
     * @param commitID
     * @return blobMap
     */
    public static HashMap<String, String> loadFile(String commitID){

        Tree treeObj = Tree.deserialize(commitID);
        String[] treeValue = treeObj.getValue().split("\\s+");
        HashMap<String, String> blobMap = new HashMap<>();
        HashMap<String, String> treeMap = new HashMap<>();
        listFiles(blobMap, treeMap, treeValue);
        return blobMap;
    }


    /**
     * Store the file name and hash value in HashMap
     * @param blobMap
     * @param treeMap
     * @param text
     * @return void
     */
    public static void listFiles(HashMap<String, String> blobMap, HashMap<String, String> treeMap, String[]text)
    {
        for(int i = 1; i < text.length; i+=4) {
            if(text[i].equals("blob"))
                blobMap.put(text[i+2], text[i+1]);
            else if(text[i].equals("tree"))
                treeMap.put(text[i+2], text[i+1]);
        }

        if(treeMap.size()!=0) {
            for (String key : treeMap.keySet()) {
                Tree temp = Tree.deserialize(key);
                text = temp.getValue().split("\\s+");
                HashMap<String, String> tempmap = new HashMap<>();
                listFiles(blobMap,tempmap, text); // Recursively find the files in the folder and add to the hashmap
                treeMap.remove(key);
            }
        }
    }


    /**
     * Compare the file differences between two branches.Usually one of the branches is the LCA of the two branches.
     * @param anotherBlobMap
     * @param lcaBlobMap
     * @param printRes
     * @return int[]
     */
    public static int[] compareTwoChanges(HashMap<String, String> anotherBlobMap, HashMap<String, String> lcaBlobMap, StringBuilder printRes) throws Exception
    {

        int[] res = new int[3]; // 0/1/2: the number of changed files/ inserted rows/ deleted rows

        for(String key: anotherBlobMap.keySet()){
            if(lcaBlobMap.containsKey(key)){
                String file1Hash = anotherBlobMap.get(key);
                String file2Hash = lcaBlobMap.get(key);
                if( ! file1Hash.equals(file2Hash) ){
                    res[0] += 1;
                    int[] temp = countChange(file1Hash, file2Hash);
                    res[1] += temp[0];
                    res[2] += temp[1];
                    printRes.append(" " + key + " | " + temp[0] + " + " + temp[1] + " -\n");
                }

            }else{
                int newAddLines = countFileLine(anotherBlobMap.get(key));
                res[0] += 1;
                res[1] += newAddLines;
                printRes.append(" " + key + " | " + newAddLines + " +\n");
            }
        }

        for(String key: lcaBlobMap.keySet()){
            if(!anotherBlobMap.containsKey(key)){
                String file2Hash = lcaBlobMap.get(key);
                int newDeleteLines = countFileLine(file2Hash);
                res[0] += 1;
                res[2] += newDeleteLines;
                printRes.append(" " + key + " | " + newDeleteLines + " -\n");
            }

        }

        return res;
    }


    /**
     * Compare the file differences between the two branches and their LCA.
     * @param lca
     * @param anotherBranch
     * @param currentBranch
     * @param printRes
     * @param conflictList
     * @param changeList
     * @return int[]
     */

    public static int[] compareThreeChanges(HashMap<String, String> lca, HashMap<String, String> anotherBranch, HashMap<String, String> currentBranch,
                                            StringBuilder printRes, ArrayList<String> conflictList, HashMap<String, String> changeList) throws Exception {
        int[] res = new int[3];
        for(String key: anotherBranch.keySet()){

            String otherFileHash = anotherBranch.get(key);
            String lcaFileHash = lca.get(key);
            String currentFileHash = currentBranch.get(key);

            if(lca.containsKey(key)){

                if( ! otherFileHash.equals(lcaFileHash) && currentBranch.containsKey(key) && lcaFileHash.equals(currentFileHash))
                {
                    // The file was only modified in another branch

                    res[0] += 1;
                    int[] temp = countChange(otherFileHash, lcaFileHash);
                    res[1] += temp[0];
                    res[2] += temp[1];
                    printRes.append(" " + key + " | " + temp[0] + " + " + temp[1] + " -\n");
                    changeList.put(key, "change");
                }
                else if ( ! otherFileHash.equals(lcaFileHash) && currentBranch.containsKey(key) && ! otherFileHash.equals(currentFileHash) && ! lcaFileHash.equals(currentFileHash))
                {
                    // The file is modified in both branches
                    conflictList.add(key);
                }

            }else if(currentBranch.containsKey(key)){

                if( ! anotherBranch.get(key).equals(currentFileHash))
                {
                    // There is a new file with the same name in both branches, but their content is different
                    conflictList.add(key);
                }

            }else {
                // There is a new file in another branch, but not in the current branch

                int newAddLines = countFileLine(otherFileHash);
                res[0] += 1;
                res[1] += newAddLines;
                printRes.append(" " + key + " | " + newAddLines + " +\n");
                changeList.put(key, "add");
            }

        }

        for(String key: lca.keySet()){

            String lcaFileHash = lca.get(key);
            String currentFileHash = currentBranch.get(key);

            if(!anotherBranch.containsKey(key) && currentBranch.containsKey(key) && currentFileHash.equals(lcaFileHash))
            {
                // Another branch deleted a file, and the file in the current branch is the same as the one in LCA

                int newDeleteLines = countFileLine(lcaFileHash);
                res[0] += 1;
                res[2] += newDeleteLines;
                printRes.append(" " + key + " | " + newDeleteLines + " -\n");
                changeList.put(key,"delete");
            }
        }

        return res;
    }


    /**
     * Calculate the number of increased or decreased lines in the file, according to the comparison result of the MyersDiff algorithm.
     * @param firstHash
     * @param secondHash
     * @return int[]
     */

    public static int[] countChange(String firstHash, String secondHash) throws Exception {
        Blob firstBolb = Blob.deserialize(firstHash), secondBlob = Blob.deserialize(secondHash);
        String firstContent = firstBolb.getValue(), secondContent = secondBlob.getValue();
        int[] res = new int[2];
        List<String> changeLabel = JitDiff.strChange(secondContent, firstContent);
        for(String diff:changeLabel){
            if(diff.equals("+"))
                res[0]++;
            else if(diff.equals("-"))
                res[1]++;
        }

        return res;
    }


    /**
     * Calculate how many lines are in the file.
     * @param blobHash
     * @return int
     */

    public static int countFileLine(String blobHash) {
        String fileContent = Blob.deserialize(blobHash).getValue();
        String str1 = fileContent.replaceAll("\n", "");
        return fileContent.length() - str1.length() + 1;
    }

}